<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Annotated Presentation Creator</title>
    <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap">
    <style>
      :root {
        --primary-color: #4361ee;
        --primary-hover: #3a56d4;
        --secondary-color: #f72585;
        --text-color: #2b2d42;
        --light-bg: #f8f9fa;
        --border-color: #dee2e6;
        --success-color: #38b000;
        --warning-color: #ff9f1c;
        --radius: 8px;
        --shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
      }

      * {
        box-sizing: border-box;
        margin: 0;
        padding: 0;
      }

      body {
        font-family: 'Inter', system-ui, -apple-system, sans-serif;
        line-height: 1.6;
        color: var(--text-color);
        background-color: #fff;
        padding: 0;
        margin: 0;
      }

      .container {
        width: 100%;
        max-width: 1200px;
        margin: 0 auto;
        padding: 20px;
      }

      header {
        background-color: var(--light-bg);
        border-bottom: 1px solid var(--border-color);
        padding: 1.5rem 0;
        margin-bottom: 2rem;
      }

      h1, h2, h3 {
        font-weight: 600;
        line-height: 1.3;
        margin-bottom: 1rem;
        margin-top: 1rem;
        color: var(--text-color);
      }

      h1 {
        font-size: 2.2rem;
        margin-bottom: 0.5rem;
      }

      p {
        margin-bottom: 1.2rem;
      }

      a {
        color: var(--primary-color);
        text-decoration: none;
        transition: color 0.2s;
      }

      a:hover {
        color: var(--secondary-color);
        text-decoration: underline;
      }

      /* Controls section */
      .controls-section {
        background-color: var(--light-bg);
        border-radius: var(--radius);
        padding: 1.5rem;
        margin-bottom: 2rem;
        box-shadow: var(--shadow);
      }

      .controls-group {
        display: flex;
        flex-wrap: wrap;
        gap: 1rem;
        align-items: center;
        margin-bottom: 1rem;
      }

      .ocr-progress {
        margin-top: 1rem;
      }

      button, .file-input-wrapper {
        display: inline-flex;
        align-items: center;
        justify-content: center;
        background-color: var(--primary-color);
        color: white;
        border: none;
        border-radius: var(--radius);
        padding: 0.6rem 1.2rem;
        font-size: 0.95rem;
        font-weight: 500;
        cursor: pointer;
        transition: all 0.2s ease;
        box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
      }

      button:hover {
        background-color: var(--primary-hover);
        transform: translateY(-1px);
      }

      button#restoreButton {
        background-color: var(--warning-color);
      }

      .file-input-wrapper {
        position: relative;
        overflow: hidden;
      }

      .file-input-wrapper input[type="file"] {
        position: absolute;
        font-size: 100px;
        opacity: 0;
        right: 0;
        top: 0;
        cursor: pointer;
      }

      progress {
        width: 100%;
        height: 10px;
        border-radius: 5px;
        margin-top: 0.5rem;
      }

      /* Slides section */
      .bundle {
        display: flex;
        flex-direction: column;
        align-items: flex-start;
        background-color: white;
        border-radius: var(--radius);
        margin-bottom: 2.5rem;
        border: 1px solid var(--border-color);
        overflow: hidden;
        box-shadow: var(--shadow);
      }

      .slide-image {
        display: flex;
        justify-content: center;
        background-color: #f0f0f0;
        padding: 1rem;
        border-bottom: 1px solid var(--border-color);
      }

      .slide-image img {
        max-width: 100%;
        height: auto;
        border-radius: 4px;
        box-shadow: 0 2px 6px rgba(0, 0, 0, 0.1);
      }

      .slide-content {
        padding: 1.5rem;
      }

      .alt-text-container {
        margin-bottom: 1.5rem;
      }

      .annotation-container {
        border-top: 1px solid var(--border-color);
        padding-top: 1.5rem;
      }

      textarea {
        width: 100%;
        padding: 0.8rem;
        border: 1px solid var(--border-color);
        border-radius: var(--radius);
        font-family: inherit;
        font-size: 0.95rem;
        line-height: 1.4;
        transition: border-color 0.2s;
        resize: vertical;
      }

      textarea:focus {
        outline: none;
        border-color: var(--primary-color);
        box-shadow: 0 0 0 3px rgba(67, 97, 238, 0.1);
      }

      .textarea-alt {
        height: 100px;
        margin-bottom: 1rem;
      }

      .textarea-annotation {
        height: 150px;
        margin-top: 1rem;
      }

      .annotation-preview {
        background-color: var(--light-bg);
        padding: 1rem;
        border-radius: var(--radius);
        margin-bottom: 1rem;
        border: 1px solid var(--border-color);
      }

      /* Template section */
      .template-section {
        background-color: var(--light-bg);
        border-radius: var(--radius);
        padding: 1.5rem;
        margin: 2rem 0;
        box-shadow: var(--shadow);
      }

      .template-section h2 {
        margin-top: 0;
      }

      .template-section textarea {
        height: 150px;
        font-family: monospace;
        margin: 1rem 0;
      }

      pre {
        background-color: #f1f1f1;
        padding: 1rem;
        border-radius: var(--radius);
        overflow-x: auto;
        font-size: 0.9rem;
        margin: 1rem 0;
        border: 1px solid var(--border-color);
      }

      /* Template output */
      #templateOutput {
        width: 100%;
        height: 300px;
        margin-top: 1.5rem;
        font-family: monospace;
      }

      /* Responsive styles */
      @media (min-width: 768px) {
        .bundle {
          flex-direction: row;
        }

        .slide-image {
          width: 40%;
          border-right: 1px solid var(--border-color);
          border-bottom: none;
        }

        .slide-content {
          width: 60%;
        }
      }

      @media (max-width: 767px) {
        h1 {
          font-size: 1.8rem;
        }

        .controls-group {
          flex-direction: column;
          align-items: stretch;
        }

        button, .file-input-wrapper {
          width: 100%;
        }
      }
    </style>
    <script src="https://cdn.jsdelivr.net/npm/marked@4.3.0/lib/marked.umd.min.js"></script>
    <script
      defer
      data-domain="til.simonwillison.net"
      src="https://plausible.io/js/plausible.js"
    ></script>
  </head>
  <body>
    <header>
      <div class="container">
        <h1>Annotated Presentation Creator</h1>
        <p>Create beautiful annotated slides for your presentations. See <a href="https://simonwillison.net/2023/Aug/6/annotated-presentations/">How I make annotated presentations</a> for instructions.</p>
      </div>
    </header>

    <div class="container">
      <div id="controls" class="controls-section">
        <p>Upload your presentation slides below. Enter "skip" as alt text to skip a slide.</p>
        <div class="controls-group">
          <div class="file-input-wrapper">
            Choose Images
            <input type="file" id="imageInput" multiple accept="image/*" />
          </div>
          <button id="loadImagesButton">Load Images</button>
          <button id="restoreButton" style="display: none;">Restore saved content</button>
        </div>
        <div class="ocr-progress" style="display: none;">
          <button id="ocrMissingButton">OCR Missing Alt Text</button>
          <progress id="ocrProgressBar" value="0" max="80"></progress>
        </div>
      </div>

      <div id="preview"></div>

      <div id="templateTool" class="template-section">
        <h2>Generate HTML from Your Slides</h2>
        <p>
          Execute the following template against the slides on the page. An
          <code>escapeHtml()</code> function is available.
        </p>
        <textarea id="templateToRun">
<div class="slide">
  <img src="${filename}" alt="${escapeHtml(alt)}">
  ${markdownAsHtml}
</div></textarea>
        <button id="executeTemplatesButton">Execute Template</button>
        
        <h3>Example Template</h3>
        <p>Here's a common template pattern:</p>
<pre>&lt;div class="slide" id="${filename}"&gt;
  &lt;img loading="lazy" src="https://static.simonwillison.net/static/2025/building-apps-on-llms/${filename}" alt="${escapeHtml(alt)}" style="max-width: 100%" /&gt;
  &lt;div&gt;&lt;a style="float: right; text-decoration: none; border-bottom: none; padding-left: 1em;" href="https://simonwillison.net/2025/May/15/building-on-llms/#${filename}"&gt;#&lt;/a&gt;
  ${markdownAsHtml}
  &lt;/div&gt;
&lt;/div&gt;</pre>
      </div>
    </div>

    <script type="module">
      var images = [];

      document
        .getElementById("loadImagesButton")
        .addEventListener("click", createSlidesForImages);

      const ocrButton = document.getElementById("ocrMissingButton");
      ocrButton.addEventListener("click", ocrMissingAltText);
      const progressBar = document.querySelector("#ocrProgressBar");

      async function ocrMissingAltText() {
        // Load Tesseract
        var s = document.createElement("script");
        s.src = "https://unpkg.com/tesseract.js@v2.1.0/dist/tesseract.min.js";
        document.head.appendChild(s);

        s.onload = async () => {
          const images = document.getElementsByTagName("img");
          const worker = Tesseract.createWorker();
          await worker.load();
          await worker.loadLanguage("eng");
          await worker.initialize("eng");
          ocrButton.innerText = "Running OCR...";
          ocrButton.disabled = true;

          // Iterate through all the images in the output div
          for (const img of images) {
            const altTextarea = img.closest('.bundle').querySelector(".textarea-alt");
            // Check if the alt textarea is empty
            if (altTextarea.value === "") {
              const imageUrl = img.src;
              var {
                data: { text },
              } = await worker.recognize(imageUrl);
              altTextarea.value = text; // Set the OCR result to the alt textarea
              progressBar.value += 1;
            }
          }

          await worker.terminate();
          ocrButton.innerText = "OCR Complete";
          ocrButton.disabled = false;
        };
      }

      function createSlidesForImages() {
        // Clear previous data
        var previewDiv = document.getElementById("preview");
        images = [];
        previewDiv.innerHTML = "";

        var files = document.getElementById("imageInput").files;
        if (files.length === 0) {
          alert("Please select at least one image file");
          return;
        }

        var processedCount = 0;
        Array.from(files).forEach(function (file) {
          var reader = new FileReader();
          reader.onload = function (e) {
            var base64content = e.target.result;
            var filename = file.name;
            // Append to the images array
            images.push({ filename: filename, base64content: base64content });

            processedCount++;
            if (processedCount === files.length) {
              allFilesProcessed();
            }
          };
          reader.readAsDataURL(file);
        });

        function allFilesProcessed() {
          // Sort the images array by filename
          images.sort((a, b) => a.filename.localeCompare(b.filename));

          // Create slides for each image
          images.forEach((image) => {
            var div = document.createElement("div");
            div.classList.add("bundle");
            
            // Create image container
            var imgContainer = document.createElement("div");
            imgContainer.classList.add("slide-image");
            
            var imgTag = document.createElement("img");
            imgTag.src = image.base64content;
            imgTag.alt = "";
            imgContainer.appendChild(imgTag);
            
            // Create content container
            var contentContainer = document.createElement("div");
            contentContainer.classList.add("slide-content");
            
            // Alt text section
            var altTextContainer = document.createElement("div");
            altTextContainer.classList.add("alt-text-container");
            
            var altLabel = document.createElement("label");
            altLabel.textContent = "Alt Text for Accessibility:";
            altLabel.htmlFor = `image-${image.filename}-alt`;
            
            var altTextarea = document.createElement("textarea");
            altTextarea.placeholder = "Describe the slide content for screen readers...";
            altTextarea.className = "textarea-alt";
            altTextarea.id = `image-${image.filename}-alt`;
            altTextarea.name = `image-${image.filename}-alt`;
            altTextarea.dataset.filename = image.filename;
            altTextarea.dataset.name = "alt";
            
            altTextContainer.appendChild(altLabel);
            altTextContainer.appendChild(altTextarea);
            
            // Annotation section
            var annotationContainer = document.createElement("div");
            annotationContainer.classList.add("annotation-container");
            
            var annotationLabel = document.createElement("label");
            annotationLabel.textContent = "Your Annotation (supports Markdown):";
            annotationLabel.htmlFor = `image-${image.filename}-annotation`;
            
            var annotationPreview = document.createElement("div");
            annotationPreview.className = "annotation-preview";
            annotationPreview.dataset.name = "markdownAsHtml";
            
            var annotationTextarea = document.createElement("textarea");
            annotationTextarea.placeholder = "Add your notes, explanations, or context using Markdown...";
            annotationTextarea.className = "textarea-annotation";
            annotationTextarea.id = `image-${image.filename}-annotation`;
            annotationTextarea.dataset.filename = image.filename;
            annotationTextarea.name = `image-${image.filename}-annotation`;
            annotationTextarea.dataset.name = "annotation";
            annotationTextarea.addEventListener("input", function () {
              updatePreview(this);
            });
            
            annotationContainer.appendChild(annotationLabel);
            annotationContainer.appendChild(annotationPreview);
            annotationContainer.appendChild(annotationTextarea);
            
            // Assemble the content container
            contentContainer.appendChild(altTextContainer);
            contentContainer.appendChild(annotationContainer);
            
            // Assemble the main bundle
            div.appendChild(imgContainer);
            div.appendChild(contentContainer);
            
            previewDiv.appendChild(div);
          });

          document.querySelector(".ocr-progress").style.display = "block";
          progressBar.setAttribute("max", images.length);

          addRestoreButton();
        }

        function updatePreview(textarea) {
          var previewDiv = textarea.closest('.annotation-container').querySelector('.annotation-preview');
          var html = marked
            .parse(textarea.value, { headerIds: false, mangle: false })
            .trim();
          previewDiv.innerHTML = html || '<em>Your annotation preview will appear here...</em>';
        }
      }

      // Variable to store previous state of textareas
      var previousTextareaState = "";

      // Function to save textareas
      function saveTextAreas() {
        var textareas = document.querySelectorAll("textarea");
        var savedTextAreas = {};

        textareas.forEach(function (textarea) {
          if (textarea.name && textarea.value.trim() !== "") {
            savedTextAreas[textarea.name] = textarea.value;
          }
        });

        var currentState = JSON.stringify(savedTextAreas);
        if (currentState !== previousTextareaState && currentState != "{}") {
          localStorage.setItem("savedTextAreas", currentState);
          previousTextareaState = currentState;
        }
      }

      // Function to restore textareas
      function restoreTextAreas() {
        var savedTextAreas = JSON.parse(
          localStorage.getItem("savedTextAreas") || "{}",
        );

        for (var key in savedTextAreas) {
          var textarea = document.querySelector(`textarea[name="${key}"]`);
          if (textarea) {
            textarea.value = savedTextAreas[key];
            // Trigger markdown rendering
            textarea.dispatchEvent(new Event('input', {
              'bubbles': true,
              'cancelable': true
            }));
          }
        }
      }

      // Function to add restore button
      function addRestoreButton() {
        var savedTextAreas = JSON.parse(
          localStorage.getItem("savedTextAreas") || "{}",
        );
        var count = Object.keys(savedTextAreas).filter(
          (key) => savedTextAreas[key].trim() !== "",
        ).length;
        
        var button = document.querySelector("#restoreButton");
        
        if (count > 0) {
          button.style.display = "inline-flex";
          button.textContent = `Restore ${
            count === 1 ? "1 saved item" : `${count} saved items`
          }`;
          button.addEventListener("click", restoreTextAreas);
        } else {
          button.style.display = "none";
        }
      }

      setInterval(saveTextAreas, 1000);

      // Check if there's saved textareas in localStorage and add restore button
      window.addEventListener("load", addRestoreButton);

      function gatherData(selector) {
        const elements = document.querySelectorAll(selector);
        const dataObjects = [];
        elements.forEach((element) => {
          const inputs = Array.from(
            element.querySelectorAll("textarea,[data-name]"),
          );
          const elementData = {};
          inputs.forEach((el) => {
            if (el.dataset["name"] && el.tagName.toLowerCase() == "div") {
              elementData[el.dataset["name"]] = el.innerHTML.trim();
              return;
            }
            const elementName = el.dataset["name"] || el.getAttribute("name");

            // Get the value of the input/textarea element
            const elementValue = el.value;

            // Add the name-value pair to the data object
            elementData[elementName] = elementValue;

            // Get data attributes (attributes starting with "data-")
            const dataAttributes = el.dataset;
            for (const attribute in dataAttributes) {
              if (attribute != "name") {
                elementData[attribute] = dataAttributes[attribute];
              }
            }
          });
          if (elementData.alt && elementData.alt.toLowerCase() == 'skip') {
            // Skip this slide
          } else {
            dataObjects.push(elementData);
          }
        });
        return dataObjects;
      }
      window.gatherData = gatherData;

      function executeTemplates(template, arrayOfObjects) {
        return arrayOfObjects.map((obj) => {
          let keys = Array.from(Object.keys(obj));
          let values = Array.from(Object.values(obj));
          keys.push("escapeHtml");
          values.push(escapeHtml);
          const templateFunction = new Function(
            ...keys,
            `return \`${template}\`;`,
          );
          return templateFunction(...values);
        });
      }

      document
        .querySelector("#executeTemplatesButton")
        .addEventListener("click", executeAllTemplates);

      function executeAllTemplates() {
        var template = document.querySelector("#templateToRun");
        try {
          var objects = gatherData(".bundle");
          var strings = executeTemplates(template.value, objects);
        } catch (e) {
          alert("Error: " + e.message);
          return;
        }
        var textarea = document.querySelector("#templateOutput");
        if (!textarea) {
          textarea = document.createElement("textarea");
          textarea.setAttribute("id", "templateOutput");
          textarea.className = "template-output";
          document.getElementById("templateTool").appendChild(textarea);
        }
        textarea.value = strings.join("\n");
      }
      
      function escapeHtml(s) {
        if (!s) return "";
        return s
          .replace(/&/g, "&amp;")
          .replace(/</g, "&lt;")
          .replace(/>/g, "&gt;")
          .replace(/"/g, "&quot;")
          .replace(/'/g, "&#39;");
      }
    </script>
  </body>
</html>
